/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
class Base {

    public static final int TIME_MULTIPLIER = 1000;
    public static final int TOBASE64_NUMBER_2 = 2;
    public static final int TOBASE64_NUMBER_3 = 3;
    public static final int TOBASE64_NUMBER_4 = 4;
    public static final int TOBASE64_NUMBER_6 = 6;
    public static final int TOBASE64_NUMBER_8 = 8;
    public static final int TOBASE64_NUMBER_25 = 25;
    public static final int TOBASE64_NUMBER_97 = 97;
    public static final int TOBASE64_NUMBER_120 = 20;
    public static final int TOBASE64_NUMBER_8192 = 8192;
    public static final int TOBASE64_NUMBER_16384 = 16384;
    public static final int TOBASE64_NUMBER_0X03 = 0x03;
    public static final int TOBASE64_NUMBER_0X0F = 0x0f;
    public static final int TOBASE64_NUMBER_0X3F = 0x3f;
    public static final int TOBASE64_NUMBER_0X7F = 0x7f;
    public static final int TOBASE64_NUMBER_0XFF = 0xff;

    public static boolean inDebug = false;
    public static void log(String str) {
        if (inDebug) {
            System.out.println(str);
        }
    }
    public static double currentTimestamp13() {
        return System.nanoTime() / 1_000_000.0;
    }

    /* Convert data (an array of integers) to a Base64 string. */
    public static String toBase64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    public static String base64Pad = "=";

    public static String toBase64(String data) {
        String resultStr = "";
        int length = data.length();
        int i = 0;

        // Convert every three bytes to 4 ascii characters.
        while (i < length - TOBASE64_NUMBER_2) {
            resultStr += toBase64Table.charAt(data.charAt(i) >> TOBASE64_NUMBER_2);
            resultStr +=
                    toBase64Table.charAt(
                    ((data.charAt(i) & TOBASE64_NUMBER_0X03) << TOBASE64_NUMBER_4) +
                    (data.charAt(i + 1) >> TOBASE64_NUMBER_4)
            );
            resultStr +=
                    toBase64Table.charAt(
                    ((data.charAt(i + 1) & TOBASE64_NUMBER_0X0F) << TOBASE64_NUMBER_2) +
                            (data.charAt(i + TOBASE64_NUMBER_2) >> TOBASE64_NUMBER_6)
                    );
            resultStr += toBase64Table.charAt(data.charAt(i + TOBASE64_NUMBER_2) & TOBASE64_NUMBER_0X3F);
            i += TOBASE64_NUMBER_3;
        }

        // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
        if (length % TOBASE64_NUMBER_3 > 0) {
            i = length - (length % TOBASE64_NUMBER_3);
            resultStr += toBase64Table.charAt(data.charAt(i) >> TOBASE64_NUMBER_2);
            if (length % TOBASE64_NUMBER_3 == TOBASE64_NUMBER_2) {
                resultStr +=
                        toBase64Table.charAt(
                        ((data.charAt(i) & TOBASE64_NUMBER_0X03) << TOBASE64_NUMBER_4) +
                        (data.charAt(i + 1) >> TOBASE64_NUMBER_4)
                        );
                resultStr += toBase64Table.charAt((data.charAt(i + 1) & TOBASE64_NUMBER_0X0F) << TOBASE64_NUMBER_2);
                resultStr += base64Pad;
            } else {
                resultStr += toBase64Table.charAt((data.charAt(i) & TOBASE64_NUMBER_0X03) << TOBASE64_NUMBER_4);
                resultStr += base64Pad + base64Pad;
            }
        }

        return resultStr;
    }

    /* Convert Base64 data to a string */
    public static short[] toBinaryTable = {
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, //binary1
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, //binary2
            52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1,
            -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, //binary3
            15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
            -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, //binary4
            41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
    };

    public static String base64ToString(String data) {
        String result = "";
        int leftbits = 0; // number of bits decoded, but yet to be appended
        int leftdata = 0; // bits decoded, but yet to be appended

        // Convert one by one.
        for (int i = 0; i < data.length(); i++) {
            int c = toBinaryTable[ data.charAt(i) & TOBASE64_NUMBER_0X7F];
            boolean padding = data.charAt(i) ==  base64Pad.charAt(0);
            // Skip illegal characters and whitespace;
            if (c == -1) {
                continue;
            }

            // Collect data into leftdata, update bitcount;
            leftdata = (leftdata << TOBASE64_NUMBER_6) | c;
            leftbits += TOBASE64_NUMBER_6;

            // If we have 8 or more bits, append 8 bits to the result;
            if (leftbits >= TOBASE64_NUMBER_8) {
                leftbits -= TOBASE64_NUMBER_8;
                // Append if not padding;
                if (!padding) {
                    result += (char) ((leftdata >> leftbits) & TOBASE64_NUMBER_0XFF);
                }
                leftdata &= (1 << leftbits) - 1;
            }
        }

        // If there are any bits left, the base64 string was corrupted;
        if (leftbits > 0) {
            //log("Corrupted base64 string");
        }
        return result;
    }

    public static void run() {
        String str = "";

        for (int i = 0; i < TOBASE64_NUMBER_8192; i++) {
            str += (char) (TOBASE64_NUMBER_25 * Math.random() + TOBASE64_NUMBER_97);
        }

        int i = TOBASE64_NUMBER_8192;
        while (i <= TOBASE64_NUMBER_16384) {
            String base64 = "";
            base64 = toBase64(str);
            //log("string-base64: toBase64 " + i + " = " + base64);
            String encoded = base64ToString(base64);
            //log("string-base64: toBase64 " + i + " = " + encoded);
            if (!encoded.equals(str)) {
                throw new Error("bad result: expected " + str + " but got " + encoded);
            }
            // Double the string
            str += str;
            i *= TOBASE64_NUMBER_2;
        }
    }
}

/*
 * @State
 * @Tags Jetstream2
 */
class Benchmark {
    /*
     * @Benchmark
     */
    public void runIteration() {
        for (int i = 0; i < Base.TOBASE64_NUMBER_8; i++) {
            double startTimeInLoop = Base.currentTimestamp13();
            Base.run();
            double endTimeInLoop = Base.currentTimestamp13();
            // log("base64: index= " + i + " time= " + (endTimeInLoop - startTimeInLoop));
        }
    }
	
    public static void base64() {
        double startTime = Base.currentTimestamp13();
        Benchmark benchmark = new Benchmark();
        for (int i = 0; i < Base.TOBASE64_NUMBER_120; i++) {
            benchmark.runIteration();
        }
        double endTime = Base.currentTimestamp13();
        System.out.println("base64: ms = " + (endTime - startTime));

    }

    public static void main(String[] args) {
        base64();
    }
}
